<?php
###########################################################################
#  Copyright (C) 2006-2007 Glyphix, Inc. <briz@glyphix.com>               #
#                                                                         #
#  Permission is hereby granted, free of charge, to any person obtaining  #
#  a copy of this software and associated documentation files (the        #
#  "Software"), to deal in the Software without restriction, including    #
#  without limitation the rights to use, copy, modify, merge, publish,    #
#  distribute, sublicense, and/or sell copies of the Software, and to     #
#  permit persons to whom the Software is furnished to do so, subject to  #
#  the following conditions:                                              #
#                                                                         #
#  The above copyright notice and this permission notice shall be         #
#  included in all copies or substantial portions of the Software.        #
#                                                                         #
#  Except as contained in this notice, the name(s) of the above           #
#  copyright holders shall not be used in advertising or otherwise to     #
#  promote the sale, use or other dealings in this Software without       #
#  prior written authorization.                                           #
#                                                                         #
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        #
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     #
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. #
#  IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR      #
#  OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,  #
#  ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR  #
#  OTHER DEALINGS IN THE SOFTWARE.                                        #
###########################################################################

/**
 * GXPage Website Framework
 *
 * A simple Website framework that aims to take full advantage of W3C standards.
 * GXPage generates a single XML document for a request and then transforms that document with your XSL stylesheet(s).
 * It's designed to separate data from code from presentation by ruthlessly enforcing the use of XML.
 * @author		Brad Brizendine <briz@glyphix.com>
 * @link		http://www.glyphix.com/
 * @license		http://opensource.org/licenses/bsd-license.php BSD License
 * @version		0.5
 * @package		GXPage
 * @subpackage	Engine
 * @copyright	Copyright 2006 - 2007, Glyphix, Inc.
 * @uses		SingletonPattern
 */

/**
 * GXPage
 *
 * This is the central GXPage class, responsible for threading all functionality into a single execution call.
 */
include_once( dirname(__FILE__) .'/gxpage-functions.php' );

class GXPage extends SingletonPattern {

	// to be used in gxpage-runtime
	private $GXPageVersion = '0.5';

	// For the config xml files
	private static $SystemConfig;
	private static $SiteConfig;

	// The main objects used by GXPage
	private static $GXObject;
	private static $GXOutput;
	private static $GXXSL;
	private static $GXCookie;

	// The default language if nothing is specified in SystemConfig
	public static $Lang = 'en-US';

	// The main objects for this run
	private static $GXError;
	private static $Run;		// GXPage-level settings, including environment and request
	private static $Layout;		// File system layout of this Website
	private static $Page;		// <Page> node config
	private static $Action;		// <Action> node config
	private static $Theme;		// <Theme> node config
	private static $Params;		// Params and their values pulled from System.xml
	private static $Modules;	// Instantiated modules

	// gxpage-utilities XSL file
	private static $GXPageXSL;

	/**
	 * getInstance
	 *
     * Returns the single unique instance of this class.
	 * @return object unique instance of this object
     */
	public static function getInstance(){
		return SingletonPattern::getSingleton( __CLASS__ );
	}

	/**
	 * Constructor
	 *
     * Verifies paths, sets up bootstrap config, prepares for showPage().
	 * Layout:
	 *  + Client: relative paths for use in JavaScript inside the browser
	 *  + Server: relative client paths when transform is set to Client, absolute server paths when transform is set to Server
	 *  + FS: always absolute server paths
	 * @return boolean true on success, false on failure
     */
	protected function __construct(){
		// load error handler
		self::$GXError		= GXError::getInstance();

		trigger_error( 'Instantiating core objects', E_USER_WARNING );

		self::$GXObject 	= GXObject::getInstance();
		self::$GXOutput 	= GXOutput::getInstance();
		self::$GXXSL	 	= GXXSL::getInstance();
		self::$GXCookie		= GXCookie::getInstance();

		trigger_error( 'Initializing GXPage Website paths', E_USER_WARNING );

		// if path_info begins with a slash, we're using mod_rewrite-style Page and Action values
		if( isset($_SERVER['PATH_INFO']) && substr($_SERVER['PATH_INFO'],0,1) == '/' ){
			$postpath = $_SERVER['PATH_INFO'];
			// subtract postpath from php_self so dirname will work as expected
			$self = dirname( substr($_SERVER['PHP_SELF'],0,-(strlen($postpath))) );
		}else{
			$self = dirname($_SERVER['PHP_SELF']);
		}

		// Get the request uri...if this site is buried within another directory, store the directory name
		if( strlen($self['dirname']) > 1 ){
			$self = substr($self['dirname'],1,strlen($self['dirname']));
		}
		// Add a trailing slash if no extension on the end and the last character is not a directory separator
		if( !strpos($self,'.php') && substr($self,-1,1) != DIRECTORY_SEPARATOR ){
			$self .= '/';
		}
		// turn backslashes into forward to support windows
		$self = str_replace(array('/','\\'),DIRECTORY_SEPARATOR,$self);

		// Using script_filename instead of document_root to handle ~user directories
		$script = pathinfo($_SERVER['SCRIPT_FILENAME']);
		// Add trailing slash to dirname
		$script['dirname'] .= DIRECTORY_SEPARATOR;
		// Build the full URI to be able to include protocol://location/ in javascript and html.
		$protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
		$uri = $protocol .'://' .$_SERVER['HTTP_HOST'] .str_replace(array('/','\\'),'/',$self);

		// determine if gxpage is within the website's directory
		$scriptPath = dirname(realpath($_SERVER['SCRIPT_FILENAME']));
		$gxpagePath = realpath(GXPAGE_INCLUDE_PATH);
		$scriptAr = explode( DIRECTORY_SEPARATOR, realpath($_SERVER['SCRIPT_FILENAME']) );
		$gxpageAr = explode( DIRECTORY_SEPARATOR, realpath(GXPAGE_INCLUDE_PATH) );
		// remove the first empty item
		array_shift($scriptAr);
		array_shift($gxpageAr);

		// get the name of the script's enclosing directory and determine if all prior directories are the same
		// reverse the arrays
		$within = true;
		for( $i = 0; $i < count($scriptAr); $i++ ){
			// if we're not examining index.php AND the path parts do not match, we're not within
			if( $scriptAr[$i] != $script['basename'] && $scriptAr[$i] != $gxpageAr[$i] ){
				$within = false;
				break;
			}
		}
		// if within, put the rest of the paths together
		if( $within ){
			// remove index.php
			array_pop($scriptAr);
			$GXPagePath = join( DIRECTORY_SEPARATOR, array_diff($scriptAr,$gxpageAr) ) .'GXPage' .DIRECTORY_SEPARATOR;
		}

		trigger_error('GXPage is ' .(($within) ? 'within' : 'outside') .' the website directory', E_USER_WARNING );

		// Add Client paths for use in JavaScript and HTML
		$paths['Client'] = array(
			// force urls for Client to be forward slashes
			'Root'		=> str_replace(DIRECTORY_SEPARATOR,'/',$self),
			'Self'		=> str_replace(DIRECTORY_SEPARATOR,'/',$script['basename']),
			'GXPage'	=> str_replace(DIRECTORY_SEPARATOR,'/',$GXPagePath),
			'BaseHREF'	=> $uri			// full URI to the website root
			);
		// Add Server paths; absolute if Transform is set to Server; relative if Transform is set to Client
		$paths['Server'] = array(
			'Root'		=> $self,
			'Self'		=> $script['basename'],
			'GXPage'	=> $GXPagePath
			);
		// Add file system paths -- always absolute paths to each item
		$paths['FS'] = array(
			'Root'		=> $script['dirname'],
			'Self'		=> $script['dirname'] .$script['basename'],
			'GXPage'	=> GXPAGE_INCLUDE_PATH .DIRECTORY_SEPARATOR .'GXPage' .DIRECTORY_SEPARATOR
			);

		trigger_error( 'Loading SystemConfig (' .$paths['FS']['Root'] .')', E_USER_WARNING );

		// Load GX config
		if( !(self::$SystemConfig = new GXPage_SystemConfig( $paths['FS']['Root'] .SYSTEM_CONFIG_PATH )) ){
			trigger_error( 'Unable to initialize system config', E_USER_ERROR );
			return $this->throwError();
		}
		// SystemConfig::xpath() calls GXDOMExec::xpath
		$xmlsettings = self::$SystemConfig->xpath('/System/GXPage/XML/*');
		foreach( $xmlsettings as $item ){
			$run[ $item->nodeName ] = $item->nodeValue;
		}
		$run['Debug']		= self::$SystemConfig->xpath('/System/GXPage/Debug', 0)->nodeValue;
		if( !is_numeric($run['Debug']) ){
			$run['Debug'] = 0;
		}

		// set debug level
		trigger_error( 'Setting log level to ' .$run['Debug'], E_USER_WARNING );
		self::$GXError->setLogLevel($run['Debug']);

		// set the default php timezone
		$tz = self::$SystemConfig->xpath('/System/GXPage/Timezone', 0);
		$run['Timezone']	= ($tz) ? $tz->nodeValue : 'America/Los_Angeles';
		date_default_timezone_set($run['Timezone']);
		trigger_error( 'Set default timezone to ' .$run['Timezone'], E_USER_WARNING );

		// add Run params
		$run['GXPage-Version']	= $this->GXPageVersion;
		$run['Host']			= strtolower($_SERVER['HTTP_HOST']);
		$run['Protocol']		= $protocol;
		$run['Timestamp']		= date('Y-m-d H:i:s');
		// default the language ... this::generateGXPageDoc will override
		$run['Language']		= 'en-US';
		$run['Country']			= 'US';

		// if GXPage is not within the website, force the transform to be Server
		if( !$within && $run['Transform'] == 'Client' ){
			trigger_error( 'GXPage is not inside Website directory; client-side transformation cannot be used with this setup', E_USER_WARNING );
			$run['Transform'] = 'Server';
			unset($paths['Client']['GXPage']);
		}

		// Read and store layout directories
		$tmp = self::$SystemConfig->xpath('/System/Site/Layout/*');
		foreach( $tmp as $item ){
			$Layout[ $item->nodeName ] = $item->nodeValue;
		}
		// Add Layout items to URL
		foreach( $Layout as $name => $val ){
			$paths['Client'][$name] = $val;
			if( !strpos($val,'.') ){
				$paths['Client'][$name] .= '/';
			}
		}
		// Add Layout items to Server
		// if we're transforming on the server, use absolute paths
		// xsl requires forward slashes for directory separator, even on windows
		if( $run['Transform'] == 'Server' ){
			$dirSep = '/';
			$paths['Server']['Root'] = $script['dirname'];
			// prefix all paths with the absolute FS path to the website directory
			$prefix = str_replace('\\','/',$paths['Server']['Root']);
			$paths['Server']['GXPage'] = $prefix .$GXPagePath;
		}else{
			$dirSep = '/';
			$paths['Server']['Root'] = $self;
			// prefix all paths with the relative Client path to the website directory
			$prefix = $paths['Client']['Root'];
			$paths['Server']['GXPage'] = $GXPagePath;
		}
		foreach( $Layout as $name => $val ){
			$paths['Server'][$name] = str_replace(array('\\','/'),$dirSep,$prefix .$val);
			if( !strpos($val,'.') ){
				$paths['Server'][$name] .= '/';
			}
		}
		// Add Layout items to FS
		foreach( $Layout as $name => $val ){
			$paths['FS'][$name] = str_replace(array('/','\\'),DIRECTORY_SEPARATOR,$paths['FS']['Root'] .$val);
			if( !strpos($val,'.') ){
				$paths['FS'][$name] .= DIRECTORY_SEPARATOR;
			}
		}

		trigger_error( 'Loading SiteConfig (' .$paths['FS']['SiteConfig'] .')', E_USER_WARNING );

		// Load the config for this site
		if( !(self::$SiteConfig	 = new GXPage_SiteConfig( $paths['FS']['SiteConfig'] )) ){
			trigger_error( 'Unable to load Site config. Exiting.', E_USER_ERROR );
			return $this->throwError();
		}

		trigger_error( 'Setting include path to ' .realpath($paths['FS']['Library']) .PATH_SEPARATOR .get_include_path(), E_USER_WARNING );

		// Build the include path...
		// Set the include_once path to include Library.
		set_include_path( realpath($paths['FS']['Library']) .PATH_SEPARATOR .get_include_path() );

		// Store the Layout object
		self::$Layout = arr2obj($paths);
		// Store the Run object
		self::$Run = arr2obj($run);

		// load the gxpage-utilities XSL file
		self::$GXPageXSL = new GXDOM( self::$Layout->FS->GXPage .'XSL' .DIRECTORY_SEPARATOR .'gxpage-utilities.xsl' );

		trigger_error( 'GXPage initialization is complete', E_USER_WARNING );

		return true;
	}

	/**
	 * __call
	 *
	 * Attempts to instantiate the object named @method with @args.
	 * Only works when not in static mode.
	 * @param string $method the name of the method to call, passed to __call by PHP
	 * @param array $args the list of arguments passed to __call by PHP
	 * @return mixed
	 */
	public function __call( $method = null, $args = null ){
		// check for GXPage's objects
		if( isset(self::${$method}) ){
			return self::${$method};
		}
		// check for modules
		if( isset(self::$Modules) ){
			if( $obj = self::$Modules->$method($args) ){
				return $obj;
			}
		}
		// last resort, try to instantiate it with GXObject
		if( $obj = GXObject::get($method,$args) ){
			return $obj;
		}
		// couldn't find anything matching method
		trigger_error( 'Unable to locate an object or property by that name (' .$method .')', E_USER_ERROR );
		return false;
	}

	/**
	 * __get
	 *
	 * Calls __call.
	 * Only works when not in static mode.
	 * @param string $name the name of the class var to return, passed to __get by PHP
	 * @return mixed
	 */
	public function __get( $name = null ){
		return $this->$name();
	}

	/**
	 * get
	 *
	 * Returns the requested static core object.
	 * @param string $name the name of the requested core object
	 * @return mixed the object or false
	 */
	public static function get( $name = null ){
		if( isset(self::${$name}) ){
			return self::${$name};
		}
		trigger_error( 'No variable by that name (' .$name .')', E_USER_ERROR );
		return false;
	}

	/**
	 * validatePath
	 *
     * Attempts to determine if the referenced Path exists on the Web server.
	 * If System.xml is set to transform on the Client, the path will most likely be relative.
	 * @param string $path file path
	 * @return string validated path
     */
	public function validatePath( $path = null ){
		if( !$path ){
			trigger_error( 'Missing path', E_USER_ERROR );
			return false;
		}

		trigger_error( 'Validating path ' .$path, E_USER_WARNING );

		// If we're starting at the top, just use the provided path (preg is for C:\)
		if( substr($path,0,1) == DIRECTORY_SEPARATOR || preg_match('/^[a-z]{1}:/i',$path) ){
			$fullPath = $path;
		}else{
			$fullPath = self::$Layout->FS->Root .$path;
		}
		// handle cross-platform directory separator
		$fullPath = str_replace( array('/','\\'), DIRECTORY_SEPARATOR, $fullPath );

		if( !is_file($fullPath) ){
			trigger_error( 'Not a file: ' .$fullPath .', testing for directory', E_USER_WARNING );
			if( !is_dir($fullPath) ){
				trigger_error( 'Not a directory: ' .$fullPath, E_USER_ERROR );
				return false;
			}
		}
		return $fullPath;
	}

	/**
	 * xsl
	 *
	 * Wrapper to GXXSL::process.
	 * @param mixed $xml the xml to be processed, as an object or a string
	 * @param mixed $sheet the stylesheet to use as the processor, as an object or a string
	 * @param boolean $gxruntime whether to add the gxpage-runtime node before transforming
	 * @return mixed whatever is specified as the return value or false on failure
	 */
	function xsl( $xml = null, $sheet = null, $gxruntime = true ){
		// automatically handle file path for sheet
		if( !is_file($sheet) ){
			$basepath = self::$Theme->getConfig()->exec->xpath('BasePath',0)->nodeValue;
			$sheet = self::$Layout->FS->Root .$basepath .$sheet;
		}

		// get the root node
		$docroot = $xml->exec->xpath('//*[1]',0);

		// add the gxpage-runtime option
		if( $gxruntime ){
			$GXdoc = $this->generateGXPageDoc();
			// Combine remaining xml docs
			$msgs = $this->getMessages();
			if( $msgs ){
				$GXdoc->Messages($msgs,GXDOM::NO_CONTAINER);
			}

			// add the gxdoc to the root element of this page
			$docroot->gxpage( $GXdoc, GXDOM::NO_CONTAINER );
		}

		// return gxxsl process
		return self::$GXXSL->process( $docroot, $sheet );
	}

	/**
	 * load
	 *
	 * Loads a page or an action, depending on the presence of the second parameter.
	 * This actually returns the results from the load, not just the Site.xml config.
	 * @param string $page
	 * @param string $action (optional)
	 * @return object GXDOM or false on failure
	 */
	function load( $page = null, $action = null ){
		if( !$page ){
			trigger_error( 'Missing page', E_USER_ERROR );
			return false;
		}
		// load the item
		$r = self::$SiteConfig->loadItem($page,$action);
		// if it's an object, it was successful
		// if it's GXPage, we have to call load on it
		if( get_class($r) == 'GXPage_Page' ){
			$r->load();
		}
		return $r;
	}

	/**
	 * showPage
	 *
     * Processes everything and generates the page XML.
	 * Once XML is collected, it sends it to the Output object.
     * It does not return anything, it simply outputs the final result.
	 * @return nothing; this function exits
     */
	public function showPage(){
		trigger_error( 'Starting showPage()', E_USER_WARNING );

		// Build this app's auto track params
		self::$Params = GXPage_Params::getInstance();

		// Load modules
		self::$Modules = new GXPage_Modules();

		// Main page variable to store the page object
		$pageObj = null;
		// Output var default
		$output = (self::get('Run')->Transform == 'Server') ? 'HTML' : 'XML';
		// Get the page we're coming from
		$fromPage = self::$GXCookie->get('GXPage');
		if( !$fromPage ){
			$fromPage = self::$SiteConfig->getDefaultPage();
		}
		// Get the requested page
		// if we have no value for Page, default it to the first <Page> node in Site.xml
		$Page = (self::$Params->get('Page')) ? self::$Params->get('Page') : self::$SiteConfig->getDefaultPage();
		$Action = self::$Params->get('Action');

		trigger_error( 'Processing ' .$Page .'/' .$Action, E_USER_WARNING );

		// if we can't find the page/action, let the web server handle the request
		// this could be due to using mod_rewrite-style url's for Page/Action without using mod_rewrite
		if( !self::$SiteConfig->exists($Page,$Action) ){
			// rebuild the correct url and redirect
			$url = self::$Layout->Client->Root .$_SERVER['PATH_INFO'];
			trigger_error( 'Page/Action (' .$Page .'/' .$Action .') not found, redirecting to ' .$url, E_USER_ERROR );
			header( 'Location: ' .$url );
			exit();
		}

		// check permissions
		if( !self::$SiteConfig->isAuthorized($Page,$Action) ){
			trigger_error( 'User is not authorized to view ' .$Page .'/' .$Action, E_USER_ERROR );
			trigger_error( 'You are not authorized to view ' .$Page .((strlen($Action)) ? '/' .$Action : null), E_USER_NOTICE );
			$newPage = self::$SiteConfig->getDefaultPage();
			// if the new page is the same as the requested page, cannot continue or we'll end up in an infinite redirect loop
			if( $newPage == $Page ){
				trigger_error( 'This page cannot be shown.', E_USER_NOTICE );
				return $this->throwError();
				exit();
			}
			// if we're here, we can show the default page
			$Page = $newPage;
			$Action = null;
		}

		// Main action variable to store the action object
		$actionObj = null;
		// File var, used in Action to override the xml file to be used by page
		$File = null;
		// xpath pointing to node in File which will be replaced by results from action
		$replaceXpath = null;
		// the result of the top-level action
		$actionResult = null;
		// the output value specified by the action
		$actionOutput = null;

		// Main theme variable to store the theme object
		$themeObj = null;

		// load the theme ... has to happen prior to action, or the action won't be able to access xsl files without using absolute paths
		if( !$themeObj = new GXPage_Theme($Page,$Action) ){
			trigger_error( 'Unable to load the theme for ' .$Page .'/' .$Action, E_USER_ERROR );
		}
		self::$Theme = $themeObj;

		// if we have an action, it should be called first
		if( $Action ){
			trigger_error( 'Processing action ' .$Page .'/' .$Action, E_USER_WARNING );

			// actionResult might call GXPage::showPage() with its own onSuccess or onFailure value, which would stop execution of this function
			$actionObj = new GXPage_Action( $Page, $Action );

			// get the action's config
			$actionConfig = $actionObj->getConfig();
			// get the action's result
			$actionResult = $actionConfig->exec->xpath('Result',0)->nodeValue;
			// whether to redirect or not
			$actionRedirect = null;
			$actionProtocol = null;

			// set the page to the value set in the action
			if( $actionResult == 'ERROR' ){
				$toPage = $actionConfig->exec->xpath('onFailure/Page',0)->nodeValue;
				$actionRedirect = $actionConfig->exec->xpath('onFailure/Redirect',0)->nodeValue;
				$actionProtocol = $actionConfig->exec->xpath('onFailure/Protocol',0)->nodeValue;
				trigger_error( 'Action (' .$Page .'/' .$Action .') failed, returning page ' .$toPage, E_USER_WARNING );
			}else{
				$toPage = $actionConfig->exec->xpath('onSuccess/Page',0)->nodeValue;
				$actionRedirect = $actionConfig->exec->xpath('onSuccess/Redirect',0)->nodeValue;
				$actionProtocol = $actionConfig->exec->xpath('onSuccess/Protocol',0)->nodeValue;
				trigger_error( 'Action (' .$Page .'/' .$Action .') succeeded, returning page ' .$toPage, E_USER_WARNING );
			}

			// check for special directives in toPage
			if( substr($toPage,0,1) == '@' ){
				$toPage = $fromPage;
			}

			// if redirect, perform it and exit
			if( $actionRedirect ){
				trigger_error( $Page .'/' .$Action .' requests a redirect, redirecting to ' .$toPage, E_USER_WARNING );
				// if we have a protocol, build an entire absolute url
				header( 'Location: ' .( ($actionProtocol) ? $actionProtocol .'://' .self::$Run->Host : null ) .self::$Layout->Client->Root .self::$Layout->Client->Self .'/' .$toPage );
				exit();
			}

			// if we're here, we did not redirect
			$Page = $toPage;

			// record the new page in params
			// without doing so, it will look like the page we're displaying is the one in which this action is nested
			self::$Params->Page = $Page;

			// get the replace xpath
			$replaceXpath = $actionConfig->exec->xpath('Replace',0)->nodeValue;
			// set the action's output
			$actionOutput = $actionConfig->exec->xpath('Output',0)->nodeValue;

			// check for a File value to override the destination page
			if( $tmp = $actionConfig->exec->xpath('File',0)->nodeValue ){
				$File = $tmp;
				trigger_error( 'Using File value found in action ' .$Page .'/' .$Action .' (' .$File .')', E_USER_WARNING );
			}

		}

		// if we're here, actionResult returned a value, so initialize the page it set
		if( !($pageObj = new GXPage_Page($Page)) ){
			trigger_error( 'Unable to initialize this page (' .$Page .')', E_USER_ERROR );
			return $this->throwError();
		}
		// load the page
		if( !$pageObj->load($File) ){
			// get the default page, and if it's the same as the page we just got an error on, screech to a halt
			$default = self::$SiteConfig->getDefaultPage();
			if( self::$Params->Page == $default ){
				trigger_error( 'Unable to load this page (' .$Page .')', E_USER_ERROR );
				return $this->throwError();
			}
			// otherwise, kill the current page and action and show page again
			self::$Params->Page = $default;
			self::$Params->Action = null;
			trigger_error( 'That page (' .$Page .') could not be loaded, returning the default page', E_USER_NOTICE );
			return $this->showPage();
		}
		// if we have a replace xpath, send the action's results to the page
		if( $replaceXpath ){
			// if it worked, unset action because its results are now in page and all we want is page
			if( $pageObj->setContent( $replaceXpath, $actionObj->getResult() ) ){
				unset($actionObj);
			}else{
				trigger_error( 'Unable to embed content', E_USER_ERROR );
			}
		}

		// determine output
		$pageOutput = $pageObj->xpath('Output',0);
		$output = is_object($pageOutput) ? $pageOutput->nodeValue : $output;
		// if the action specified a particular type of output, let it override the page's value
		if( $actionOutput ){
			trigger_error( 'Action (' .$Page .'/' .$Action .') states the output should be ' .$actionOutput, E_USER_WARNING );
			$output = $actionOutput;
		}

		// fire an error if the action object returned a boolean but did not set a page
		if( isset($actionObj) ){
			// if the result is boolean, get rid of the action so that we select the correct output further on
			if( $actionResult == 'ERROR' || gettype($actionObj->getResult()) == 'boolean' ){
				trigger_error( 'actionObj returned ' .gettype($actionObj->getResult()), E_USER_ERROR );
				unset($actionObj);
				unset($Action);
			}
		}

		// set the main objects in this instance of GXPage
		self::$Page		= $pageObj;
		self::$Action	= isset($actionObj) ? $actionObj : null;	// avoid throwing an E_STRICT warning

		// get the stylesheet
		if( !($sheet = self::$SiteConfig->getStyleSheet($Page,$Action)) ){
			trigger_error( 'Unable to get stylesheet', E_USER_ERROR );
		}

		// Store this page
		// If there's an action, and it was an AJAX action, we don't want to set the page cookie to the page containing the AJAX action.
		if( !is_object(self::$Action) ){
			self::$GXCookie->set( 'GXPage', $Page );
		}

		// set the payload to the result of either the primary action or the primary page
		$payload = (is_object(self::$Action)) ? self::$Action->getResult() : self::$Page->getPage();

		// if output is RAW, just send the result we got from the action
		// otherwise, process it as if it's a full page result
		if( !is_numeric(strpos($output,'RAW')) ){
			trigger_error( 'Output is not RAW, including gxpage-runtime node', E_USER_WARNING );
			// complete the stylesheet path
			if( self::get('Run')->Transform == 'Server' || is_numeric(strpos($output,'HTML')) ){
				// use the entire server-side path
				$sheet = str_replace( array('/','\\'), DIRECTORY_SEPARATOR, self::$Layout->FS->Root .$sheet );
			}else{
				// use an absolute path to handle mod_rewrite-style layout
				$sheet = self::$Layout->Client->Root .$sheet;
			}
			// set the stylesheet directive
			self::$Page->getPage()->exec->styleSheet( $sheet );

			// once we're done loading the xml, add the GX environment stuff.
			$GXdoc = $this->generateGXPageDoc( self::$Page->getConfig() );

			// Combine remaining xml docs
			$msgs = $this->getMessages();
			if( $msgs ){
				$GXdoc->Messages($msgs,GXDOM::NO_CONTAINER);
			}

			// add the gxdoc to the root element of this page
			$docroot = self::$Page->getPage()->exec->xpath('//*[1]',0);
			$docroot->gxpage( $GXdoc, GXDOM::NO_CONTAINER );

			// if no theme or stylesheet, just show the xml so far
			if( !self::$Theme || !$sheet ){
				trigger_error( 'Missing theme or stylesheet; displaying GXPage XML', E_USER_ERROR );
				print(self::$GXOutput->process( 'XML', $payload ));
				flush();
				exit();
			}
		}

		// now process the result
		trigger_error( 'About to send output: ' .$output .' using ' .$sheet, E_USER_WARNING );
		if( !($result = self::$GXOutput->process( $output, $payload, $sheet )) ){
			trigger_error( 'Unable to handle output', E_USER_ERROR );

			// show xml if we're sending html and the transform died
			if( $output == 'HTML' ){
				print( self::$GXOutput->process( 'XML', $payload, $sheet ) );
				exit();
			}

			// otherwise, throw system error
			return $this->throwError();
		}

		print($result);
		flush();
		// record the final final page build time
		$buildtime = ((int)time() == (int)$_SERVER['REQUEST_TIME']) ? 'less than one second' : ((int)time() - (int)$_SERVER['REQUEST_TIME']) .' seconds';
		trigger_error( 'Final page build took ' .$buildtime, E_USER_WARNING );
		exit();
	}

	/**
	 * genGX
     * Builds the GXPage XML node.
	 * @param object $pageConfig the GXDOM config for the current page
	 * @return object GXDOM
     */
	public function generateGXPageDoc( $pageConfig = null ){
		trigger_error( 'Starting generateGXPageDoc()', E_USER_WARNING );
		if(!self::$Params){
			trigger_error( 'Missing Params', E_USER_WARNING );
			return false;
		}
		// create the GX node
		$PDoc = new GXDOM('gxpage-runtime');

		// create the Run node
		$RunDoc = $PDoc->Run(self::$Run);

		// create the request node
		// Full is the entire recursive request
		$RequestDoc = $RunDoc->Request();
		// include the full request
		$RequestDoc->Full($_REQUEST);
		// add request nodes...use Name parameter to avoid potential node naming conflicts
		$flatNode = $RequestDoc->Flat();
		$flat = $this->flattenURI( $_REQUEST );
		for( $i = 0; $i < count($flat); $i++ ){
			$flatNode->Key( urldecode($flat[$i]['Val']), array('Name'=>$flat[$i]['Key']) );
		}

		// add params to Run
		$ParamsDoc = $RunDoc->Params( self::$Params->getConfig(), GXDOM::NO_CONTAINER );

		// add the layout node ... Layout is an array
		$PDoc->Layout( self::get('Layout') );

		// add the action
		if( self::$Action ){
			$PDoc->Action( self::$Action->getConfig() );
		}

		// add the theme node ... Theme is a GXDOM object
		$PDoc->Theme( self::$Theme->getConfig(), GXDOM::NO_CONTAINER );
		// build the site node
		$SiteDoc = $PDoc->Site();
		// if there are Include sections in the config which are part of active modules or sections, process them
		$includes = self::$SystemConfig->xpath('//Include[ not(ancestor-or-self::*/@RunOnLoad="0") ]/*');
		if( !is_array($includes) ){
			$includes = array();
		}
		// if there's an Include section in the Page config
		if( $pageConfig ){
			if( $pageIncludes = $pageConfig->exec->xpath('Include/*') ){
				$includes = array_merge($includes,$pageIncludes);
			}
			if( is_array($includes) && count($includes) ){
				trigger_error( 'Found ' .count($includes) .' Includes', E_USER_WARNING );
				foreach( $includes as $item ){
					// load any params which might be defined in the <Include>
					self::$Params->loadCallerParams($item);
					// then run the specified page/action
					$Page 	= $item->attributes['Page'];
					$Action = $item->attributes['Action'];
					// call method
					if( !($actionResult = new GXPage_Action( $Page, $Action )) ){
						trigger_error( 'Unable to load Include action: ' .$Page .'/' .$Action , E_USER_ERROR );
					}
					$autoInclude[] = $actionResult->getResult();
				}
			}
			if( is_array($autoInclude) && count($autoInclude) ){
				foreach( $autoInclude as $result ){
					if( $result ){
						$SiteDoc->include( $result, GXDOM::NO_CONTAINER );
					}
				}
			}
		}

		// Include site info
		if( $tmp = self::$SystemConfig->xpath( '/System/Site/i18n/Languages/*' ) ){
			$i18n = $SiteDoc->i18n();
			foreach( $tmp as $lang ){
				$parts = explode('-',$lang->attributes['code']);
				$country = array(
					'Code'		=> $lang->attributes['code'],
					'Language'	=> $parts[0],
					'Country'	=> $parts[1]
					);
				$i18n->lang( $lang->nodeValue, $country );
			}
		}
		unset($tmp);
		$info = self::$SystemConfig->xpath('/System/Site/Info/*');
		foreach( $info as $item ){
			$tmp[ $item->nodeName ] = $item->nodeValue;
		}
		$SiteDoc->Info($tmp);

		trigger_error( 'Completed ' .__METHOD__, E_USER_WARNING );
		return $PDoc;
	}

	/**
	 * flattenURI
	 *
     * Takes an array of key=>value pairs and turns it into a flattened array.
	 * If there are sub-arrays, they will be flattened into a string.
	 * @param string $uri the request for this page as pulled from the web server
	 * @return array
     */
	function flattenURI( $uri = null ){
		if( !$uri ){
			trigger_error( 'Missing uri', E_USER_WARNING );
			return false;
		}
		$str = http_build_query( $uri );
		// replace encoded '[' and ']'
		$str = str_replace('%5B','[',$str);
		$str = str_replace('%5D',']',$str);
		// explode on ampersand
		$tmp = explode( '&', $str );
		for( $i = 0; $i < count($tmp); $i++ ){
			// explode on equals
			$param = explode( '=', $tmp[$i] );
			$flat[] = array(
				'Key' => $param[0],
				'Val' => $param[1]
				);
		}
		return $flat;
	}

	/**
	 * throwError
	 *
     * Called when a catastrophic error occurs.
	 * This simply spits out the current messages and exits.
	 * @return exit()
     */
	function throwError(){
		// include paths
		trigger_error( 'Layout: ' .print_r(self::$Layout,true), E_USER_WARNING );
		$err = new GXDOM();
		$err->{'gxpage-error'}( $this->getMessages() );
		$r = self::$GXOutput->process( 'HTML', $err, self::$Layout->FS->GXPage .'XSL' .DIRECTORY_SEPARATOR .'Error.xsl' );
		print($r);
		flush();
		exit();
	}

	/**
	 * getMessages
	 *
	 * Grabs messages from the GXError object.
	 * @return object GXDOM
	 */
	function getMessages(){
		// record the final final page build time
		$buildtime = ((int)time() == (int)$_SERVER['REQUEST_TIME']) ? 'less than one second' : ((int)time() - (int)$_SERVER['REQUEST_TIME']) .' seconds';
		trigger_error( 'Page build took ' .$buildtime, E_USER_WARNING );
		$msgs = new GXDOM('Messages');
		$msgs->err( self::$GXError->getErrors(), GXDOM::NO_CONTAINER );
		return $msgs;
	}

}

?>